/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { ArkFile, AstTreeUtils, ts } from 'arkanalyzer';

import { AdviceChecker, BaseMetaData } from '../BaseChecker';
import { Defects } from '../../Index';
import { AstMatcher, AstMatcherCallback, MatcherCallback, MatcherTypes } from '../../Index';
import { Rule } from '../../Index';
import { RuleListUtil } from '../../utils/common/DefectsList';
import { IssueReport } from '../../model/Defects';


const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: 'docs/no-this-alias.md',
    description: 'Disallow aliasing this.'
};

const defaultOptions: Options = {
    allowDestructuring: true,
    allowedNames: [],
};

interface Options {
    allowDestructuring?: boolean;
    allowedNames?: string[];
}

export class NoThisAliasCheck implements AdviceChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private options: Options = defaultOptions;

    private thisAliasMatcher: AstMatcher = {
        matcherType: MatcherTypes.AST,
        match: (node) => this.isThisAlias(node)
    };

    public beforeCheck(): void {
        const userOptions = this.rule?.option?.[0] as Options || {};
        this.options = {
            ...defaultOptions,
            ...userOptions,
        };
    }

    public afterCheck(): void {
        // Cleanup if needed
    }

    public registerMatchers(): MatcherCallback[] {
        const astMatcherCallback: AstMatcherCallback = {
            matcher: this.thisAliasMatcher,
            callback: this.check,
        };
        return [astMatcherCallback];
    }

    private isThisAlias(node: ts.Node): boolean {
        if (!ts.isVariableDeclaration(node) && !ts.isBinaryExpression(node)) {
            return false;
        }

        const children = node.getChildren();
        if (children.length === 0) {
            return false;
        }

        // Check if the last child is 'this' keyword
        return children[children.length - 1].kind === ts.SyntaxKind.ThisKeyword;
    }

    public check = (node: ts.Node, file: ArkFile): void => {
        const children = node.getChildren();
        const firstNode = children[0];
        const message = this.getAliasMessage(firstNode);

        if (message) {
            this.addIssueReportNode(firstNode, file, message);
        }
    };

    private getAliasMessage(firstNode: ts.Node): string | undefined {
        // Allow destructuring if configured
        if (ts.isObjectBindingPattern(firstNode) || ts.isArrayBindingPattern(firstNode)) {
            return this.options.allowDestructuring
                ? undefined
                : 'Unexpected aliasing of members of \'this\' to local variables.';
        }

        // Check if the variable name is in the allowed list
        const name = firstNode.getText();
        return this.options.allowedNames?.includes(name)
            ? undefined
            : 'Unexpected aliasing of \'this\' to local variable.';
    }

    private addIssueReportNode(node: ts.Node, file: ArkFile, message: string): void {
        const sourceFile = AstTreeUtils.getSourceFileFromArkFile(file);
        if (!sourceFile) {
            return;
        }

        const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart());
        const severity = this.rule.alert ?? this.metaData.severity;
        const line = pos.line + 1;
        const column = pos.character + 1;
        const endColumn = column + node.getText().length;

        const defect = new Defects(
            line,
            column,
            endColumn,
            message,
            severity,
            this.rule.ruleId,
            file.getFilePath(),
            this.metaData.ruleDocPath,
            true,
            false,
            false
        );

        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }
}